﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;

namespace SES_Server
{
    public class SMTPSession
    {
        public TcpClient client;
        public string clientDomain;
        public int lastCommand;

        //The To, From and, Data to be used when sending the email
        public string TO;
        public string FROM;
        public string DATA;

        public bool transmissionComplete = false;
        public string recievedData = "";
        private byte[] buffer = new byte[1024];

        public void Send(string response)
        {
            if (client.Connected)
            {
                StreamWriter sw = new StreamWriter(client.GetStream());
                sw.AutoFlush = true;
                sw.WriteLine(response);
            }
        }
        public void Recieve(bool isRetry = false)
        {
            if (client.Connected)
            {
                NetworkStream ns = client.GetStream();
                buffer = new byte[1024];
                try
                {
                    ns.BeginRead(buffer, 0, buffer.Length, ReadComplete, null);
                }
                catch (Exception ex)
                {
                    new ErrorLogger().WriteErrorLog(ex);
                    //if the connection has been closed but all required data has been recieved, send the message
                    if (this.lastCommand == 6)
                    {
                        this.recievedData = "SENDNOQUIT";
                        this.transmissionComplete = true; return;
                    }
                }
            }
        }
        public void ReadComplete(IAsyncResult ar)
        {
            NetworkStream ns = client.GetStream();
            int bytesRead = 0;
            try
            {
                bytesRead = ns.EndRead(ar);
            }
            catch (Exception ex)
            {
                new ErrorLogger().WriteErrorLog(ex);
                //if the connection has been closed but all required data has been recieved, send the message
                if (this.lastCommand == 6)
                {
                    this.recievedData = "SENDNOQUIT";
                    this.transmissionComplete = true;
                }
            }
            if (bytesRead == 0)
            {
                transmissionComplete = true;
                return;
            }
            else
            {
                recievedData = String.Concat(recievedData, Encoding.ASCII.GetString(buffer, 0, bytesRead));

                if (recievedData.EndsWith("\r\n\r\n.") || recievedData.EndsWith("\r\n\r\n.\r\n") && lastCommand == 6)
                {
                    transmissionComplete = true; return;
                }
                else if (recievedData.EndsWith("\r\n") && lastCommand != 6)
                {
                    transmissionComplete = true; return;
                }
                transmissionComplete = false;
                Recieve();
            }
        }


    }
    public class SMTPProcessor
    {
        //Command codes
        private const int cmd_helo = 0;
        private const int cmd_rset = 1;
        private const int cmd_noop = 2;
        private const int cmd_quit = 3;
        private const int cmd_mail = 4;
        private const int cmd_rcpt = 5;
        private const int cmd_data = 6;

        //Responses
        private const string msg_welcome = "220 {0} Welcome";
        private const string msg_helo = "250 {0}";
        private const string msg_ok = "250 OK";
        private const string msg_data = "354 Start mail input; end with <CRLF>.<CRLF>";
        private const string msg_goodbye = "221 Goodbye.";
        private const string msg_unknown = "500 Command unknown.";
        private const string msg_invalid_comm_order = "503 Command not allowed.";
        private const string msg_invalid_arg_count = "501 Incorrect no. arguments.";
        private const string msg_invalid_address = "451 Address is invalid.";
        private const string msg_unknown_user = "550 User does not exist.";
        private const string msg_system_error = "554 Transaction failed.";


        private readonly string AWSAccessKey = System.Configuration.ConfigurationManager.AppSettings["AWSAccessKey"];
        private readonly string AWSSecretKey = System.Configuration.ConfigurationManager.AppSettings["AWSSecretKey"];
        private TcpListener listener;
        private Thread listenThread;
        private readonly string domain = System.Configuration.ConfigurationManager.AppSettings["DomainName"];

        /// <summary>
        /// Sets up the TCPListener and starts the listening thread
        /// </summary>
        public void Start()
        {
            int listenPort = Convert.ToInt32(System.Configuration.ConfigurationManager.AppSettings["ListenPort"]);
            this.listener = new TcpListener(IPAddress.Any, (listenPort == null) ? 26 : listenPort);
            ListenForClients();
            this.listenThread = new Thread(new ThreadStart(ListenForClients));
            this.listenThread.Start();
        }

        /// <summary>
        /// Listens for and accepts TcpClients
        /// </summary>
        private void ListenForClients()
        {
            this.listener.Start();
            while (true)
            {
                TcpClient client = this.listener.AcceptTcpClient();
                new Thread(new ParameterizedThreadStart(ProcessConnection)).Start(client);
            }
        }

        /// <summary>
        /// Processes the TcpClient connection
        /// </summary>
        /// <param name="client"></param>
        private void ProcessConnection(object client)
        {
            SMTPSession session = new SMTPSession();
            session.client = (TcpClient)client;

            session.Send(String.Format(msg_welcome, this.domain));
            while (session.client.Connected)
            {
                int i = 0;
                session.Recieve();
                while (!session.transmissionComplete)
                {
                    System.Threading.Thread.Sleep(100);
                    i++;
                    if (i >= 1200)
                    {
                        session.recievedData = "SENDNOQUIT";
                        session.transmissionComplete = true;
                    }
                }
                ProcessCommand(session);

            }
            session.client.Close();
        }

        /// <summary>
        /// Processes commands sent to Tcp Server
        /// </summary>
        public void ProcessCommand(SMTPSession session)
        {
            
            string comm = session.recievedData.Trim();
            string[] args = comm.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
            if (comm == "")
            {
                session.transmissionComplete = false;
                return;
            }

             switch (args[0].ToLower())
            {
                case "helo":
                    HELO(session, args);
                    session.lastCommand = 0;
                    break;
                case "rset":
                    RSET(session);
                    session.lastCommand = 1;
                    break;
                case "noop":
                    session.Send(msg_ok);
                    session.lastCommand = 2;
                    break;
                case "quit":
                    if (session.lastCommand == 7)
                    {
                        SendEmail(session.FROM, session.DATA);
                    }
                    session.Send(msg_goodbye);
                    session.lastCommand = 3;
                    session.client.Client.Close();
                    break;
                case "mail":
                    if (args[1].ToLower().StartsWith("from"))
                    {
                        MAIL(session, comm.Substring(comm.IndexOf(" ")));
                        session.lastCommand = 4;
                        break;
                    }
                    session.Send(msg_unknown);
                    session.lastCommand = 4;
                    break;
                case "rcpt":
                    if (args[1].ToLower().StartsWith("to"))
                    {
                        RCPT(session, comm.Substring(comm.IndexOf(" ")));
                        session.lastCommand = 5;
                        break;
                    }
                    session.Send(msg_unknown);
                    session.lastCommand = 5;
                    break;
                case "data":
                    DATA(session);
                    session.lastCommand = 6;
                    break;
                case "sendnoquit":
                    if (session.lastCommand == 6 || session.lastCommand == 7)
                    {
                        SendEmail(session.FROM, session.DATA);
                        session.lastCommand = 3;
                    }
                    session.client.Client.Close();
                    break;
                default:
                    if (session.lastCommand == 6) { DATA(session); break; }
                    session.Send(msg_unknown);
                    break;
            }
            session.recievedData = "";
            session.transmissionComplete = false;
        }

        // List of possible commands 
        private void HELO(SMTPSession session, string[] args)
        {
            if (args.Length == 2)
            {
                session.clientDomain = args[1];
                session.lastCommand = cmd_helo;
                session.Send(String.Format(msg_helo, session.clientDomain));
            }
            else
            {
                session.Send(msg_invalid_arg_count);
            }
        }
        private void MAIL(SMTPSession session, string address)
        {
            bool addressValid = false;
            if (session.lastCommand == cmd_helo)
            {
                address = ParseAddress(address);
                if (address != null)
                {
                    addressValid = true;
                    session.FROM = address;
                    session.Send(msg_ok);
                }
                if (!addressValid)
                {
                    session.Send(msg_invalid_address);
                }
            }
            else
            {
                session.Send(msg_invalid_comm_order);
            }
        }
        private void RCPT(SMTPSession session, string address)
        {
            if (session.lastCommand == cmd_mail || session.lastCommand == cmd_rcpt)
            {
                address = ParseAddress(address);
                if (address != null)
                {
                    session.TO = address;
                    session.Send(msg_ok);
                }
            }
            else
            {
                session.Send(msg_invalid_comm_order);
            }
        }
        private void DATA(SMTPSession session)
        {
            if (session.lastCommand == 5)
            {
                session.Send(msg_data);
            }
            if (session.recievedData != "DATA\r\n")
            {
                session.DATA = session.recievedData;
                session.Send(msg_ok);
                session.lastCommand = 7; 
            }
        }
        private void RSET(SMTPSession session)
        {
            if (session.lastCommand != -1)
            {
                session.Send(msg_ok);
            }
            else
            {
                session.Send(msg_invalid_comm_order);
            }
        }

        /// <summary>
        /// Important Functions
        /// </summary>
        private string ParseAddress(string input)
        {
            Match match = Regex.Match(input, "(.+?)*(<.+@.+>)$", RegexOptions.IgnoreCase);
            string matchText;
            if (match.Success)
            {
                matchText = match.Groups[match.Groups.Count-1].Value.Remove(0, 1);
                matchText = matchText.Remove(matchText.Length - 1, 1);
                return matchText;
            }
            return null;
        }
        private Tuple<bool, string> SendEmail(string from, string data)
        {
            /* The following line removes forwarding headers added by IIS SMTP Server - Gmail(possibly others) will 
             * mark emails as SPAM if the header is present, so only comment it out if you have to 
             */
            data = data.Substring(data.IndexOf("MIME-Version: 1.0"));

            AmazonSES ses = new AmazonSES(AWSAccessKey, AWSSecretKey);
            return ses.SendRawEmail(from, Encoding.UTF8.GetBytes(data));
        }


    }
}
